import type { BaseRecord, DataProvider, Pagination, MetaQuery, CrudSorting, BaseKey, UpdateManyResponse, CreateManyResponse, CrudOperators } from "@refinedev/core";
import { ColumnMetadataKey, IColumn, ManyToManyMetadataKey, ManyToOneMetadataKey, OneToManyMetadataKey, OneToOneMetadataKey, createTargetMetaQuery } from "./core";
import { ProFormColumnsType } from "@ant-design/pro-form";
import { useState } from "react";
import { ExportToCsv } from "export-to-csv-fix-source-map";
import { message } from "antd";
import { Type } from './common'
import { ProColumns } from "@ant-design/pro-table";
import axios from "axios";
export type LogicalFilter = {
    field: string;
    operator: Exclude<CrudOperators, "or" | "and">;
    value: any;
};

export type ConditionalFilter = {
    key?: string;
    operator: Extract<CrudOperators, "or" | "and">;
    value: (LogicalFilter | ConditionalFilter)[];
};

export type CrudFilter = LogicalFilter | ConditionalFilter;
export type CrudFilters = CrudFilter[];

export type Fields = MetaQuery['fields']
export class Model<T extends BaseRecord> {
    get resource(): string {
        const { resource } = this.target;
        return resource
    }
    get primary(): string {
        const { primary } = this.target;
        return primary;
    }
    get primaryType(): string {
        const { primaryType } = this.target;
        return primaryType;
    }
    get displayorder() {
        const { displayorder } = this.target;
        return displayorder;
    }
    get pagination(): Pagination {
        return {}
    }
    getMeta(_fields?: Fields): MetaQuery {
        return {
            fields: this.getDefaultFields(_fields),
        }
    }
    get oneToOne() {
        return Reflect.get(this.target, OneToOneMetadataKey) || []
    }
    get oneToMany() {
        return Reflect.get(this.target, OneToManyMetadataKey) || []
    }
    get manyToMany() {
        return Reflect.get(this.target, ManyToManyMetadataKey) || []
    }
    get manyToOne() {
        return Reflect.get(this.target, ManyToOneMetadataKey) || []
    }
    get columns() {
        return Reflect.get(this.target, ColumnMetadataKey) || [];
    }
    get createColumns() {
        return this.columns.filter((it: any) => !it.hideInCreate).filter((it: any) => !it.skip)
    }
    get exportColumns(): IColumn[] {
        return this.columns.filter((it: any) => !it.hideInCreate).filter((it: any) => !it.skip).filter((it: any) => {
            if (it.isImportFile) return false;
            if (it.showInImport) return false;
            return true;
        })
    }
    get importColumns() {
        return this.columns.filter((it: any) => it.showInImport)
    }
    constructor(public dataProvider: DataProvider, public target: Type<T>) { }
    public getCreateColumns() {
        return this.createColumns.map((column: any) => this.transformerSchemaColumn(column))
    }
    public getImportColumns() {
        return this.importColumns.map((column: any) => this.transformerSchemaColumn(column))
    }
    public getImportDataColumn() {
        return this.importColumns.find((column: any) => column.isImportFile)
    }
    public transformerSchemaColumn(column: IColumn) {
        column.label = column.label || column.comment
        const { fieldProps, placeholder, rules, nullable, formItemProps, transform, ...otherProps } = column;
        const _rules = rules || [];
        if (!nullable) {
            _rules.push({ required: true, message: `${column.label}必填` })
        }
        return {
            key: column.key || column.dataIndex || column.name || column.label,
            label: column.label || column.title || column.comment,
            title: column.title || column.label || column.comment,
            dataIndex: column.dataIndex || column.name,
            transform,
            formItemProps: {
                rules: _rules,
                hidden: !!column.hide,
                ...formItemProps
            },
            fieldProps: {
                hidden: !!column.hide,
                placeholder: placeholder || `请输入${column.label}`,
                required: !column.nullable,
                ...fieldProps,
            },
            ...otherProps
        } as ProFormColumnsType
    }
    public async find(_fields?: Fields, filters?: CrudFilters, sorters?: CrudSorting, pagination?: Pagination) {
        return await this.dataProvider.getList<T>({
            resource: this.resource,
            sorters: sorters,
            filters: filters,
            pagination: pagination,
            meta: this.getMeta(_fields)
        })
    }
    public get tableFields() {
        return createTargetMetaQuery(this.target).fields;
    }
    public createFields(columns: any[]) {
        return columns.map(c => {
            const fields: any[] = [];
            if (!c.skip) {
                const { dataIndex, valueType } = c;
                if (dataIndex && valueType !== 'option') {
                    fields.push(dataIndex)
                }
            }
            return fields
        }).flat().filter(it => !!it) as any[]
    }
    public useImport() {
        const [isLoading, setIsLoading] = useState<boolean>(false)
        const handleChange = () => { }
        return {
            uploadProps: {
                onChange: handleChange,
                beforeUpload: () => false,
                showUploadList: false,
                accept: ".csv",
            },
            buttonProps: {
                type: "default",
                loading: isLoading,
            },
            isLoading,
        };
    }
    public useExportDemo(exportOptions?: any, mapData?: any) {
        const [isLoading, setIsLoading] = useState(false);
        const filename = `${this.resource}-${new Date().toLocaleString()}`;
        const triggerExport = async (limit?: number) => {
            setIsLoading(true);
            const empty = {};
            this.exportColumns.map(field => {
                Reflect.set(empty, `${field.name}`, '')
            });
            const rawData: BaseRecord[] = [];
            if (limit && limit > 0) {
                for (let i = 0; i < limit; i++) {
                    const item = {};
                    this.exportColumns.map(column => {
                        const generate = column.generate
                        if (generate) {
                            const value = generate(column);
                            Reflect.set(item, `${column.name}`, value)
                            return;
                        }
                        const _def = column.default;
                        if (typeof _def !== 'undefined') {
                            Reflect.set(item, `${column.name}`, _def)
                            return;
                        }
                        Reflect.set(item, `${column.name}`, '')
                    })
                    rawData.push(item);
                }
            } else {
                rawData.push(empty)
            }
            const csvExporter = new ExportToCsv({
                filename,
                useKeysAsHeaders: true,
                ...exportOptions,
            });
            const defaultMapData = (item: any) => item;
            csvExporter.generateCsv(rawData.map(mapData || defaultMapData));
            setIsLoading(false);
        }
        return {
            isLoading,
            triggerExport,
        };
    }
    public useExport(exportOptions?: any) {
        const [isLoading, setIsLoading] = useState(false);
        const filename = `${this.resource}-${new Date().toLocaleString()}`;
        const triggerExport = async (fields: Fields, columns: ProColumns[], mapData?: (item: any) => any, filters?: CrudFilters, sorters?: CrudSorting, maxItemCount?: number) => {
            setIsLoading(true);
            let rawData: BaseRecord[] = await this.exportFind(fields, filters, sorters, maxItemCount)
            const csvExporter = new ExportToCsv({
                filename,
                useKeysAsHeaders: false,
                useBom: true,
                showLabels: true,
                fieldSeparator: ',',
                quoteStrings: '"',
                decimalSeparator: '.',
                headers: columns.map(it => it.title),
                ...exportOptions,
            });
            if (rawData.length <= 0) {
                const empty = {};
                this.exportColumns.map(field => {
                    Reflect.set(empty, `${field.name}`, '')
                })
                rawData = [empty]
                message.error(`没有数据可以导出`);
            }
            const defaultMapData = (item: any) => {
                return columns.map(it => {
                    let itVal = ``
                    if (typeof it.dataIndex === 'string') {
                        itVal = `${item[it.dataIndex]}`
                    } else if (Array.isArray(it.dataIndex)) {
                        const keys = [...it.dataIndex];
                        let value: any = item;
                        let key: any = keys.shift()
                        while (key && value) {
                            value = value[key]
                            key = keys.shift()
                        }
                        itVal = `${value}`;
                    }
                    const enumData: any = it.valueEnum;
                    if (enumData) {
                        itVal = enumData[`${itVal}`]
                    }
                    if (typeof itVal === 'undefined') return ``
                    if (itVal === null) return ``
                    if (itVal.toLocaleLowerCase() === 'undefined') return ``
                    if (itVal.toLocaleLowerCase() === 'null') return ``
                    return itVal;
                })
            };
            csvExporter.generateCsv(rawData.map(mapData || defaultMapData));
            setIsLoading(false);
        };

        return {
            isLoading,
            triggerExport,
        };
    }
    public async exportFind(columns: Fields, filters?: CrudFilters, sorters?: CrudSorting, maxItemCount?: number) {
        let rawData: T[] = [];
        let current = 1;
        let preparingData = true;
        while (preparingData) {
            try {
                const { data, total } = await this.find(columns, filters, sorters, {
                    current,
                    pageSize: 20,
                    mode: "server",
                });
                current++;
                rawData.push(...data);
                if (maxItemCount && rawData.length >= maxItemCount) {
                    rawData = rawData.slice(0, total);
                    preparingData = false;
                }
                if (total === rawData.length) {
                    preparingData = false;
                }
            } catch (e) {
                preparingData = false;
            }
        }
        return rawData;
    }
    public async findAll(_fields: Fields, filters?: CrudFilters, sorters?: CrudSorting) {
        return await this.dataProvider.getList<T>({
            resource: this.resource,
            sorters: sorters,
            filters: filters,
            pagination: {
                mode: 'off'
            },
            meta: this.getMeta(_fields)
        })
    }
    public async findOne(_fields: Fields, filters?: CrudFilters, sorters?: CrudSorting): Promise<T | null> {
        const list = await this.find(_fields, filters, sorters, {
            current: 1,
            pageSize: 1,
            mode: 'server'
        });
        const { total, data } = list;
        if (total > 0) return data[0];
        return null;
    }
    public async getOne(id: BaseKey, _fields?: Fields) {
        return await this.dataProvider.getOne<T>({
            resource: this.resource,
            id: id,
            meta: this.getMeta(_fields)
        })
    }
    public async getMany(ids: BaseKey[], _fields?: Fields) {
        return await this.dataProvider.getMany<T>({
            resource: this.resource,
            ids: ids,
            meta: this.getMeta(_fields)
        })
    }
    public async create(variables: Partial<T>, _fields?: Fields) {
        return await this.dataProvider.create({
            resource: this.resource,
            variables: variables,
            meta: this.getMeta(_fields)
        })
    }
    public createEntity(variables: Partial<T>): T {
        const t = new this.target();
        Object.keys(variables).map(key => {
            Reflect.set(t, key, variables[key])
        });
        return t;
    }
    public createImportData(values: any) {
        const importFile = this.getImportDataColumn().dataIndex;
        const common = {}
        let data: any = [];
        Object.keys(values).map(key => {
            if (key === importFile) {
                data = values[key]
            } else {
                Reflect.set(common, key, values[key])
            }
        });
        const dataToCreate = (data: any[]) => {
            if (data.length <= 1) return [];
            const keyItems: string[] = data[0];
            const lenght = data.length;
            const list = [];
            for (let key = 1; key < lenght; key++) {
                const items = data[key];
                if (items && items.length === keyItems.length) {
                    const obj = {};
                    keyItems.map((name, index) => {
                        Reflect.set(obj, name, items[index])
                    });
                    if (Object.keys(obj).length > 0) {
                        list.push(obj)
                    }
                }
            }
            return list.filter(it => !!it);
        }
        const result = dataToCreate(data)
        return {
            common: common,
            data: result
        }
    }
    public async createMany(variables: Partial<T>[], common: Partial<T> = {}, _fields?: Fields) {
        return await this.dataProvider.createMany({
            resource: this.resource,
            variables: variables.map(v => ({ ...v, ...common })),
            meta: this.getMeta(_fields)
        })
    }
    private getDefaultFields(_fields?: Fields) {
        if (!_fields) {
            _fields = [this.primary]
        }
        if (_fields.length <= 0) {
            _fields = [this.primary]
        }
        return _fields;
    }
    private getPrimaryValue(variables: Partial<T>, id?: BaseKey) {
        if (id) return id;
        return Reflect.get(variables, this.primary)
    }
    public async update(variables: Partial<T>, id?: BaseKey, _fields?: Fields) {
        // TODO: 这里需要优化，如果存在OneToOne,OneToMany 需要单独处理
        // const oneToOne = await Promise.all(this.oneToOne.map(async column => {
        //     const { options, name, inverseSide } = column;
        //     const value = Reflect.get(variables, name);
        //     const primary = this.getPrimaryValue(variables, id);
        //     const { factory } = options;
        //     if (value) {
        //         const target = factory();
        //         const model = this.factory(target);
        //         if (primary && typeof primary !== 'object') {
        //             await model.update(value, primary)
        //         } else {
        //             await model.create(value);
        //         }
        //     }
        // }));
        return await this.dataProvider.update({
            resource: this.resource,
            id: this.getPrimaryValue(variables, id),
            variables: variables,
            meta: this.getMeta(_fields)
        });
    }
    public async updateMany(ids: BaseKey[], variables: Partial<T>, _fields?: Fields) {
        return await this.dataProvider.updateMany({
            resource: this.resource,
            ids: ids,
            variables: variables,
            meta: this.getMeta(_fields)
        })
    }
    private _getPrimaryValue(variables: Partial<T>) {
        return variables[this.primary];
    }
    public async save(variables: Partial<T>, _fields?: Fields) {
        const id = this._getPrimaryValue(variables);
        if (id) {
            return await this.dataProvider.update({
                resource: this.resource,
                id: id,
                variables: variables,
                meta: this.getMeta(_fields)
            })
        } else {
            return await this.dataProvider.create({
                resource: this.resource,
                meta: this.getMeta(_fields),
                variables: variables
            })
        }
    }
    public async saves(variables: Partial<T>[]) {
        return axios.post('/imeepos/core/web/rest/update', { resource: this.resource, variables }, {
            withCredentials: true
        }).then(res => res.data)
    }
    private _filterUpdateAndCreateData(variables: Partial<T>[]) {
        const toUpdate: { id: BaseKey, variables: Partial<T> }[] = [];
        const toInsert: any[] = [];
        variables.map(it => {
            const id = this._getPrimaryValue(it);
            if (id) {
                toUpdate.push({
                    id: id,
                    variables: it
                })
                return;
            };
            toInsert.push(it)
        });
        return [
            toUpdate,
            toInsert
        ]
    }
    public async saveMany(_fields: Fields, variables: Partial<T>[]) {
        const [toUpdate, toCreate] = this._filterUpdateAndCreateData(variables);
        let updateResult: UpdateManyResponse<T>;
        let createResult: CreateManyResponse<T>;
        if (toUpdate && toUpdate.length > 0) {
            updateResult = await this.dataProvider.updateMany<T>({
                resource: this.resource,
                ids: toUpdate.map(it => it.id),
                variables: variables,
                meta: this.getMeta(_fields)
            })
        }
        if (toCreate && toCreate.length > 0) {
            createResult = await this.dataProvider.createMany<T>({
                resource: this.resource,
                variables: toCreate,
                meta: this.getMeta(_fields)
            })
        }
        return [
            updateResult,
            createResult
        ]
    }
    public async delete(id: BaseKey, _fields?: Fields) {
        return await this.dataProvider.deleteOne({
            resource: this.resource,
            id: id,
            meta: this.getMeta(_fields)
        })
    }
    public async deleteMany(_fields: Fields, ids: BaseKey[]) {
        return await this.dataProvider.deleteMany({
            resource: this.resource,
            ids: ids,
            meta: this.getMeta(_fields)
        })
    }
    public async saveByFilters(_fields: Fields, variables: Partial<T>, filters: (data: Partial<T>) => CrudFilters) {
        const one = await this.findOne(_fields, filters(variables));
        if (one) {
            return await this.update(variables, this._getPrimaryValue(one), _fields)
        } else {
            return await this.create(variables, _fields,)
        }
    }
    public async saveManyByFilters(_fields: Fields, variables: Partial<T>[], filters: (data: Partial<T>) => CrudFilters) {
        return Promise.all(variables.map(it => this.saveByFilters(_fields, it, filters)))
    }
}